﻿using System;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;
using SPFluid.Core.Exceptions;
using SPFluid.Core.Extensions;

namespace SPFluid.Core.Utilities
{
    public static partial class Map
    {
        public static T FromDatabase<T>(object value)
        {
            if (value == null)
            {
                return default(T);
            }

            if (value is T)
            {
                return (T)value;
            }

            try
            {
                var type = typeof(T);

                if (value is string)
                {
                    var stringValue = value as string;

                    if (IsTypeOrNullable<DateTime>(type))
                    {
                        // special rule for DateTime casting
                        if (Regex.IsMatch(stringValue, @"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$"))
                        {
                            return (T)(object)SPUtility.CreateDateTimeFromISO8601DateTimeString(stringValue).ToLocalTime();
                        }

                        if (stringValue.StartsWith("0x"))
                        {
                            // In some cases DateTime field values come to the event in weird hex format
                            // This seems to occur only with Office 2007 documents
                            // The hexadecimal value represents the number of ticks since 1/1/1600
                            return
                                (T)
                                (object)
                                new DateTime(long.Parse(stringValue.Replace("0x", string.Empty).Replace("|", string.Empty), NumberStyles.HexNumber))
                                    .AddYears(1600).ToLocalTime();
                        }
                    }

                    if (IsTypeOrNullable<Guid>(type))
                    {
                        return (T)(object)new Guid(stringValue);
                    }
                }

                // convesion to bool is not part of the ChangeType conversion because ChangeType doesn't check for int bool values
                if (IsTypeOrNullable<bool>(type))
                {
                    var stringValue = Convert.ToString(value);

                    bool booleanValue;
                    if (!bool.TryParse(stringValue, out booleanValue))
                    {
                        int intValue;
                        if (int.TryParse(stringValue, out intValue))
                        {
                            booleanValue = intValue != 0;
                        }
                    }

                    return (T)(object)booleanValue;
                }

                if (type == typeof(SPFieldLookupValue))
                {
                    return (T)(object)new SPFieldLookupValue(Convert.ToString(value));
                }
                
                if (!(value is IConvertible))
                {
                    value = Convert.ToString(value, CultureInfo.InvariantCulture);
                }

                // if type is Nullable but value isn't then ChangeType will fail, thus get underlying
                // not nullable type, call ChangeType and then cast to Nullable on return
                type = type.GetUnderlyingTypeIfNullable();

                return (T)Convert.ChangeType(value, type);
            }
            catch (Exception)
            {
                throw new MappingException();
            }
        }

        public static object ToDatabase(object value)
        {
            return value;
        }

        private static bool IsTypeOrNullable<T>(Type type)
            where T : struct
        {
            return typeof(T) == type || typeof(T?) == type;
        }
    }
}